﻿namespace Arms.Framework.Properties
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;

    public static class Property
    {
        /// <summary>
        /// Gets a list of matching properties from 2 different types
        /// </summary>
        /// <param name="sourceType"></param>
        /// <param name="targetType"></param>
        /// <returns></returns>
        public static List<PropertyMap> GetMatchingProperties(Type sourceType, Type targetType)
        {
            PropertyInfo[] sourceProperties = sourceType.GetProperties();
            PropertyInfo[] targetProperties = targetType.GetProperties();

            var properties = (from s in sourceProperties
                              from t in targetProperties
                              where s.Name == t.Name &&
                                    s.CanRead &&
                                    t.CanWrite &&
                                    s.PropertyType == t.PropertyType &&
                                    (
                                      (s.PropertyType.IsValueType && t.PropertyType.IsValueType) ||
                                      (s.PropertyType == typeof(string) && t.PropertyType == typeof(string))
                                    )
                              select new PropertyMap
                              {
                                  SourceProperty = s,
                                  TargetProperty = t
                              }).ToList();
            return properties;
        }

        /// <summary>
        /// Gets the properties of this object. Ignores nulls
        /// </summary>
        /// <param name="o"></param>
        /// <returns></returns>
        public static IEnumerable<PropertyValue> GetProperties(this object o)
        {
            return o.GetProperties(false);
        }

        /// <summary>
        /// Gets the properties of this object
        /// </summary>
        /// <param name="o"></param>
        /// <param name="includeNulls">Determines whether the properties will include nulls</param>
        /// <returns></returns>
        public static IEnumerable<PropertyValue> GetProperties(this object o, bool includeNulls)
        {
            if (o != null)
            {
                PropertyDescriptorCollection props = TypeDescriptor.GetProperties(o);
                foreach (PropertyDescriptor prop in props)
                {
                    object val = prop.GetValue(o);
                    if (includeNulls || val != null)
                    {
                        yield return new PropertyValue { Name = prop.Name, Value = val, ValueType = prop.PropertyType };
                    }
                }
            }
        }

        /// <summary>
        /// Generic class which copies to its target type from a source
        /// type specified in the Copy method. The types are specified
        /// separately to take advantage of type inference on generic
        /// method arguments.
        /// </summary>
        public static class PropertyCopy<TTarget> where TTarget : class, new()
        {
            /// <summary>
            /// Copies all readable properties from the source to a new instance
            /// of TTarget.
            /// </summary>
            public static TTarget CopyFrom<TSource>(TSource source) where TSource : class
            {
                return PropertyCopier<TSource>.Copy(source);
            }

            /// <summary>
            /// Static class to efficiently store the compiled delegate which can
            /// do the copying. We need a bit of work to ensure that exceptions are
            /// appropriately propagated, as the exception is generated at type initialization
            /// time, but we wish it to be thrown as an ArgumentException.
            /// </summary>
            private static class PropertyCopier<TSource> where TSource : class
            {
                private static readonly Func<TSource, TTarget> copier;
                private static readonly Exception initializationException;

                internal static TTarget Copy(TSource source)
                {
                    if (initializationException != null)
                    {
                        throw initializationException;
                    }
                    if (source == null)
                    {
                        throw new ArgumentNullException("source");
                    }
                    return copier(source);
                }

                static PropertyCopier()
                {
                    try
                    {
                        copier = BuildCopier();
                        initializationException = null;
                    }
                    catch (Exception e)
                    {
                        copier = null;
                        initializationException = e;
                    }
                }

                private static Func<TSource, TTarget> BuildCopier()
                {
                    ParameterExpression sourceParameter = Expression.Parameter(typeof(TSource), "source");
                    var bindings = new List<MemberBinding>();
                    foreach (PropertyInfo sourceProperty in typeof(TSource).GetProperties())
                    {
                        if (sourceProperty.CanRead)
                        {
                            PropertyInfo targetProperty = typeof(TTarget).GetProperty(sourceProperty.Name);
                            if (targetProperty != null && targetProperty.CanWrite && targetProperty.PropertyType.IsAssignableFrom(sourceProperty.PropertyType))
                            {
                                bindings.Add(Expression.Bind(targetProperty, Expression.Property(sourceParameter, sourceProperty)));
                            }
                        }
                        //if (!sourceProperty.CanRead)
                        //{
                        //    continue;
                        //}
                        //PropertyInfo targetProperty = typeof(TTarget).GetProperty(sourceProperty.Name);
                        //if (targetProperty == null)
                        //{
                        //    throw new ArgumentException("Property " + sourceProperty.Name + " is not present and accessible in " + typeof(TTarget).FullName);
                        //}
                        //if (!targetProperty.CanWrite)
                        //{
                        //    throw new ArgumentException("Property " + sourceProperty.Name + " is not writable in " + typeof(TTarget).FullName);
                        //}
                        //if (!targetProperty.PropertyType.IsAssignableFrom(sourceProperty.PropertyType))
                        //{
                        //    throw new ArgumentException("Property " + sourceProperty.Name + " has an incompatible type in " + typeof(TTarget).FullName);
                        //}
                        //bindings.Add(Expression.Bind(targetProperty, Expression.Property(sourceParameter, sourceProperty)));
                    }
                    Expression initializer = Expression.MemberInit(Expression.New(typeof(TTarget)), bindings);
                    return Expression.Lambda<Func<TSource, TTarget>>(initializer, sourceParameter).Compile();
                }
            }
        }
    }
}
